/**
 * 
 */
package org.simplextensions.scanner;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.annotation.Annotation;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.simplextensions.registry.Bundle;

/**
 * @author Tomasz Krzyzak, <a
 *         href="mailto:tomasz.krzyzak@gmail.com">tomasz.krzyzak@gmail.com</a>
 * @since 2009-08-18 22:30:07
 * 
 */
public class ClasspathScanner {

	private static final Log log = LogFactory.getLog(ClasspathScanner.class);

	public ClasspathScanner() {
	}

	private boolean isJARPath(String path) {
		File file = new File(path);
		return file.isFile() && file.getName().endsWith(".jar");
	}

	private long jarTime = 0;
	private long classFileTime = 0;
	private long listenersTime = 0;

	private Collection<ClassFile> scanJARFile(Bundle bundle, String jarPath) throws ClasspathScannerException {
		log.info("Scanning jar file: " + jarPath);
		Collection<ClassFile> result = new LinkedList<ClassFile>();
		JarFile jarFile;
		try {
			jarFile = new JarFile(jarPath);
		} catch (IOException e1) {
			throw new ClasspathScannerException(jarPath, e1);
		}

		Set<String> classNames = bundle.getClassNames();
		Enumeration<JarEntry> entries = jarFile.entries();
		JarEntry nextJarEntry = null;
		try {
			while (entries.hasMoreElements()) {
				nextJarEntry = entries.nextElement();
				String name = nextJarEntry.getName();

				// log.error("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"+name);

				boolean scannEntry = false;

				if (classNames != null) {
					if (classNames.contains(name.substring(0, name.length() - (name.length() > 6 ? 6 : 0)).replace("/", "."))) {
						scannEntry = true;
					}
				} else {
					if (name.endsWith(".class")) {
						scannEntry = true;
					}
				}

				if (scannEntry) {
					InputStream inputStream = null;
					try {
						inputStream = jarFile.getInputStream(nextJarEntry);
						ClassFile classFile = new ClassFile(new DataInputStream(inputStream));
						result.add(classFile);
					} catch (Exception e) {
						throw new ClasspathScannerException(name, e);
					} finally {
						if (inputStream != null)
							try {
								inputStream.close();
							} catch (IOException e) {
								log.error(e);
							}
					}
				}
			}
		} finally {
			try {
				jarFile.close();
			} catch (IOException e) {
				log.error(e);
			}
		}
		return result;
	}

	private Collection<ClassFile> scanDirectory(Bundle bundle, File directory, String basePath) throws ClasspathScannerException {
		Collection<ClassFile> result = new LinkedList<ClassFile>();
		scanDirectory(bundle, directory, basePath, result);
		return result;
	}

	/**
	 * @param directory
	 * @param string
	 * @return
	 * @throws IOException
	 */
	private void scanDirectory(Bundle bundle, File directory, String basePath, Collection<ClassFile> classFiles)
			throws ClasspathScannerException {
		Set<String> classNames = bundle.getClassNames();

		if (directory.exists()) {
			for (File file : directory.listFiles()) {
				if (file.isFile()) {
					if (file.getName().endsWith(".class")) {
						boolean scannEntry = false;
						if (classNames != null) {
							String classFilePath;
							try {
								classFilePath = file.getCanonicalPath().substring(basePath.length() + 1);
								classFilePath = classFilePath.substring(0, classFilePath.length() - (classFilePath.length() > 6 ? 6 : 0))
										.replace(File.separatorChar, '.');
								if (classNames.contains(classFilePath)) {
									scannEntry = true;
								}
							} catch (IOException e) {
								log.error(e);
							}
						} else {
							scannEntry = true;
						}
						if (scannEntry) {
							FileInputStream fileInputStream = null;
							try {
								fileInputStream = new FileInputStream(file);
								ClassFile classFile = new ClassFile(new DataInputStream(fileInputStream));
								// scanClassFile(bundle, classFile);
								classFiles.add(classFile);
							} catch (Exception e) {
								throw new ClasspathScannerException("", e);
							} finally {
								if (fileInputStream != null)
									try {
										fileInputStream.close();
									} catch (IOException e) {
										log.error(e);
									}
							}
						}

					}
				} else
					scanDirectory(bundle, file, basePath, classFiles);
			}
		}
	}

	public void scannClassPath(Bundle bundle) throws ClasspathScannerException {

		jarTime = -System.nanoTime();
		String path = bundle.getClassesPath();
		long a = System.nanoTime();
		Collection<ClassFile> bundleClasses = null;
		try {
			if (isJARPath(path)) {
				bundleClasses = scanJARFile(bundle, path);
			} else {
				bundleClasses = scanDirectory(bundle, new File(path), new File(path).getCanonicalPath());
			}
		} catch (Exception e) {
			throw new ClasspathScannerException("when scanning classes", e);
		}
		log.info("Scanning time:" + (System.nanoTime() - a) / 1000000.0 + "ms of Bundle: " + bundle.getName());

		if (bundleClasses != null) {
			if (bundle.getClassNames() == null || bundleClasses.size() == bundle.getClassNames().size()) {
				for (ClassFile classFile : bundleClasses) {
					scanClassFile(bundle, classFile);
				}
			} else {
				Set<String> classNames = new HashSet<String>(bundle.getClassNames());
				for (ClassFile cf : bundleClasses) {
					classNames.remove(cf.getName());
				}
				Iterator<String> iterator = classNames.iterator();
				StringBuilder sb = new StringBuilder(iterator.next());
				iterator.remove();
				for (String s : classNames) {
					sb.append(",").append(s);
				}
				throw new ClasspathScannerException("Bundle \'" + bundle.getName() + "\' is not complete. Classes not found: "
						+ sb.toString());
			}
		}

		jarTime += System.nanoTime();

		log.info("JarFile scanning time: " + jarTime / 1000000.0 + " (classFileTime: " + classFileTime / 1000000.0 + "ms(listenersTime: "
				+ listenersTime / 1000000.0 + "ms))");
	}

	/**
	 * @param classFile
	 */
	private void scanClassFile(Bundle bundle, ClassFile classFile) {
		classFileTime -= System.nanoTime();
		AnnotationsAttribute attributeInfo = (AnnotationsAttribute) classFile.getAttribute(AnnotationsAttribute.invisibleTag);
		if (attributeInfo != null)
			scanClassFile(bundle, classFile, attributeInfo);
		else {
			attributeInfo = (AnnotationsAttribute) classFile.getAttribute(AnnotationsAttribute.visibleTag);
			scanClassFile(bundle, classFile, attributeInfo);
		}
		classFileTime += System.nanoTime();
	}

	/**
	 * @param classFile
	 * @param attributeInfo
	 */
	private void scanClassFile(Bundle bundle, ClassFile classFile, AnnotationsAttribute attributeInfo) {
		if (attributeInfo != null) {
			executeClassScanners(bundle, classFile, attributeInfo);
		}
	}

	private void executeClassScanners(Bundle bundle, ClassFile classFile, AnnotationsAttribute attributeInfo) {
		listenersTime -= System.nanoTime();
		for (Annotation a : attributeInfo.getAnnotations()) {
			List<IClassScanner> scanListeners = bundle.getScanListeners(a.getTypeName());
			if (scanListeners != null)
				for (IClassScanner listener : scanListeners) {
					listener.scanClass(classFile, a);
				}
			else {
				log.trace("Unsupported annotation: " + a.getTypeName());
			}
		}
		listenersTime += System.nanoTime();
	}
}
